

class MapGenerator
{
	//Map size
	private var _mapLength:int;
	private var _mapWidth:int;
	//Tiles
	private var _tileModel:GameObject;
	private var _tileLength:float;
	private var _tileWidth:float;
	//Textures
	private var _grassTexture:Texture;
	private var _sandTexture:Texture;
	//Texture positions
	private var _sandPatches:Array;
	
	//Obstacles list
	private var _obstacleModels:Array;
	private var _fixedObstaclePos:Array;
	private var _obstacleBounds:Array;
	//Hills list
	private var _hillPos:Array;
	private var _flatsPos:Array;
	
	public function MapGenerator()
	{
		mapLength = 0;
		mapWidth = 0;
		_fixedObstaclePos = new Array();
		_obstacleBounds = new Array();
		
		_hillPos = new Array();
		_flatsPos = new Array();
		
		_sandPatches = new Array();
	}
	
//================= MAP ====================
	public function generateMap():Array
	{	
		//get tile dimensions
		setupTile();
		//create generic map
		var tileMap:Array = generateGenericMap();
		
		//set up obstacles in the map
		setupObstacles();
		//create flats
		generateFlats(tileMap);
		//create hills
		generateHills(tileMap);
		//create obstacles in the map
		generateFixedObstacles(tileMap);
		//create random obstacles in the map based on bounds
		generateRandomObstacles(tileMap);
		//generate cover data
		generateCoverData(tileMap);
		//generate layer data for LoS blocking
		generateLayerData(tileMap);
		//generate sand patches
		generateSandPatches(tileMap);
		
		
		return tileMap;
	}
	
	//creates a generic 2d tile array
	//No height, all similar textures
	private function generateGenericMap():Array
	{
		//create 2d array map	
		var map:Array = new Array(_mapLength);
		var i;
		for (i = 0; i < _mapLength; i++) 
			map[i] = new Array(_mapWidth);
		
		//Parent object for all the tiles
		var tilesHolder:GameObject = new GameObject("TilesHolder");
		
		//create tiles
		for (i = 0; i < _mapLength; i++)
		{
			for (var j = 0; j < _mapWidth; j++)
			{	 
				//create tile model
				var myTile:GameObject = MonoBehaviour.Instantiate(_tileModel);
				//calculate game world coordinates
				myTile.transform.position = calcWorldCoord(i,j);
				//set to basic texture
				myTile.renderer.material.mainTexture = _grassTexture;
				//attach to parent 
				myTile.transform.parent = tilesHolder.transform;
				//attach script to model
				myTile.AddComponent(Tile);
				//set tile to layer
				myTile.layer = LayerMask.NameToLayer("Tile");
				//keep reference of the script
				map[i][j] = myTile.GetComponent(Tile);
				//setup the tile
				map[i][j].initialize(i,j,0);	
			}
		}
		//setup neighbour tiles and cover information
		for (i = 0; i < _mapLength; i++)
		{
			for (j = 0; j < _mapWidth; j++)
			{
				var currentTile:Tile = map[i][j];
				if (j+1 < _mapWidth)
					currentTile.setNeighbourTile(map[i][j+1],0); //top 
				if (j+1 < _mapWidth && i+1 < _mapLength)
					currentTile.setNeighbourTile(map[i+1][j+1],1); //top right
				if (i+1 < _mapLength)
					currentTile.setNeighbourTile(map[i+1][j],2); //right
				if (j-1 >= 0 && i+1 < _mapLength)
					currentTile.setNeighbourTile(map[i+1][j-1],3); //bottom right
				if (j-1 >= 0)
					currentTile.setNeighbourTile(map[i][j-1],4); //bottom 
				if (j-1 >= 0 && i-1 >= 0)
					currentTile.setNeighbourTile(map[i-1][j-1],5); //bottom left
				if (i-1 >= 0)
					currentTile.setNeighbourTile(map[i-1][j],6); //left	
				if (j+1 < _mapWidth && i-1 >= 0)
					currentTile.setNeighbourTile(map[i-1][j+1],7); //top left
				
			}
		}
		return map;
	}
	
	private function generateCoverData(map:Array)
	{
		//setup cover information
		for (i = 0; i < _mapLength; i++)
		{
			for (j = 0; j < _mapWidth; j++)
			{
				var currentTile:Tile = map[i][j];
				if (j+1 < _mapWidth)
				{
					if ( map[i][j+1].isObstacle())
						currentTile.setCoverDirection(Tile.COVER_DIRECTION_TOP);
				}
				if (i+1 < _mapLength)
				{
					if ( map[i+1][j].isObstacle())
						currentTile.setCoverDirection(Tile.COVER_DIRECTION_RIGHT);
				}
				if (j-1 >= 0)
				{
					if ( map[i][j-1].isObstacle())
						currentTile.setCoverDirection(Tile.COVER_DIRECTION_BOTTOM);
				}
				if (i-1 >= 0)
				{
					if ( map[i-1][j].isObstacle())
						currentTile.setCoverDirection(Tile.COVER_DIRECTION_LEFT);
				}
				
				currentTile.saveCoverData();
			}
		}
	}
	
	private function generateLayerData(map:Array)
	{
		for (var i:int = 0; i < map.length; i++)
		{
			for (var j:int = 0; j < map[0].length; j++)
			{
				if (map[i][j].getHeight() > 0)
				{
					map[i][j].gameObject.layer = LayerMask.NameToLayer("Obstacle");
				}
			}
		}
	}
	
	//calculates game world coordinates with the given array index
	private function calcWorldCoord(x:int,z:int):Vector3
	{
		return new Vector3(x*_tileLength,//x
							0, 			// y
							z*_tileWidth);// z
	
	}
	
	//set the map size based on number of tiles
	public function setMapSize(length:int, width:int)
	{
		_mapLength = length;
		_mapWidth = width;
	}
	
	
//================== OBSTACLES ====================
	//set obstacles to be generated in the map
	public function setFixedObstacle(x:int, z:int, type:int):void
	{
		//rotation 0: no change
		//rotation 1: clockwise 90
		//rotation 2: clockwise 180
		//rotation 3: clockwise 280
		//use vector4 to store the 3 variables
		_fixedObstaclePos.Push(new Vector3(x,z,type));
	}
	
	//Clear fixed obstacles
	public function resetFixedObstacles():void 
	{
		_fixedObstaclePos.Clear();
	}
	
	//Position fixed obstacles on the Array of tiles
	private function generateFixedObstacles(tileMap:Array):void
	{
		Debug.Log("Generating fixed obstacle");
		//add all fixed obstacles into the map if possible
		for (var i:int = 0; i < _fixedObstaclePos.length; i++)
		{
			var obstaclePos:Vector3 = _fixedObstaclePos[i];
			var obstacleType:Obstacle = _obstacleModels[obstaclePos.z];
			buildObstacle(obstaclePos, obstacleType, tileMap);
		}
	}
	
	//set obstacle bounds to generate obstacles
	public function setObstacleBound(minimumX:int, maximumX:int, minimumY:int, maximumY:int, numObstacles:int, obstacleType:int)
	{
		_obstacleBounds.Push(new Bound(minimumX, maximumX, minimumY, maximumY, numObstacles, obstacleType));
	}
	
	//Generate obstacles based on bounds given
	private function generateRandomObstacles(tileMap:Array):void
	{
		Debug.Log("Generating random obstacles");
		//attempt to generate random obstacles if possible
		for (var i:int = 0; i < _obstacleBounds.length; i++)
		{
			var bound:Bound = _obstacleBounds[i];
			var obstacleType:Obstacle = _obstacleModels[bound.type];
			for (var j = 0; j < bound.num; j++)
			{
				var obstaclePos:Vector2 = new Vector2(Random.Range(bound.minX, bound.maxX), Random.Range(bound.minY, bound.maxY));
				buildObstacle(obstaclePos, obstacleType, tileMap);
			}
		}
	}
	
	//build the obstacle in the map if it passes several check
	private function buildObstacle(obstaclePos:Vector3, obstacleType:Obstacle, tileMap:Array):boolean
	{
		var buildable:boolean = true;
		var layout:Array = obstacleType.getLayout();
		var offset:Vector2;
		
		//PRE-BUILDING CHECKS
		//check whether obstacle exceeds map
		if (!checkBoundExceeded(obstaclePos, obstacleType.getLayoutSize()))
		{
			//check whether height elevation within the layout matches
			//TODO
			
			//check whether the tiles under the obstacle layout already has obstacles
			for (var j:int = 0; j < layout.length; j++)
			{
				offset = layout[j];
				if (tileMap[obstaclePos.x + offset.x][obstaclePos.y + offset.y].isObstacle())
				{
					return false;
				}
				if (tileMap[obstaclePos.x + offset.x][obstaclePos.y + offset.y].getHeight() != tileMap[obstaclePos.x][obstaclePos.y].getHeight())
				{
					return false;
				}
			}
		}
		else
		{
			return false;
		}
		
		//BUILD OBSTACLE IF CHECKS PASSED
		//clone and instantiate object in the game world
		var obstacle:Obstacle = new Obstacle(obstacleType);
		obstacle.setPosition(obstaclePos.x,obstaclePos.y);
		//setup world coord for obstacle
		var worldPos:Vector3 = tileMap[obstaclePos.x][obstaclePos.y].getSurfaceCoord();
		obstacle.setWorldCoord(worldPos.x, worldPos.y, worldPos.z);
		
		//set each tile under the layout to be an obstacle tile
		for (var i:int = 0; i < layout.length; i++)
		{
			offset = layout[i];
			tileMap[obstaclePos.x + offset.x][obstaclePos.y + offset.y].setIsObstacle(true);
			
			//attach a reference to the obstacle in the tile
			tileMap[obstaclePos.x + offset.x][obstaclePos.y + offset.y].setObstacle(obstacle);
			//attach a reference to the tile in the obstacle
			obstacle.addTile(tileMap[obstaclePos.x + offset.x][obstaclePos.y + offset.y]);
		}
		
		//obstacle successfully built
		return true;
	}
	
	//check whether size exceeds given bounds
	private function checkBoundExceeded(startPos:Vector2, size:Vector2):boolean
	{
		var radius:int = size.x / 2;
		//exceed too much X
		if (startPos.x + radius >= _mapLength)
			return true;
		//too little X
		if (startPos.x - radius < 0 )
			return true;
		
		radius = size.y / 2;
		//too much Z
		if (startPos.y + radius >= _mapWidth)
			return true;
		//too little Z
		if (startPos.y - radius < 0)
			return true;
			
		return false;
	}
	

//================== HEIGHT ADJUSTMENT ================
	//add hill positions
	public function setFlatsPos(minX:int, maxX:int, minY:int, maxY:int, height:int):void
	{
		//type 0 = exactly bound size
		_flatsPos.Push(new Bound(minX, maxX, minY, maxY, height, 0));
	}
	
	public function setHillPos(xPos:int, zPos:int):void
	{
		_hillPos.Push(new Vector2(xPos, zPos));
	}
	
	//Generate flats
	private function generateFlats(tileMap:Array):void
	{
		for (var i:int = 0; i < _flatsPos.length; i++)
		{
			var bound = _flatsPos[i];
			raiseTiles(bound.minX, bound.maxX, bound.minY, bound.maxY, bound.num, tileMap);
		}
	}
	
	//Generate hills
	private function generateHills(tileMap:Array):void
	{
		for (var i:int = 0; i < _hillPos.length; i++)
		{
			var hillPos:Vector2 = _hillPos[i];
			//height 3: 2 x 2
			//height 2: heightSize.x
			//height 1: heightSize.y
			var heightSize:Vector2 = new Vector2(); //height size is the size of each flat at the respective heights
			heightSize.x = Random.Range(1,3);
			heightSize.y = Random.Range(1,3);
			
			//height 3
			raiseTiles(hillPos.x, hillPos.x+1, hillPos.y, hillPos.y+1,3,tileMap);
			//height 2
			raiseTiles(hillPos.x-heightSize.x, hillPos.x+1+heightSize.x, hillPos.y-heightSize.x, hillPos.y+1+heightSize.y, 2, tileMap);
			//height 1
			raiseTiles(hillPos.x-heightSize.x-heightSize.y, hillPos.x+1+heightSize.x+heightSize.y,
					 hillPos.y-heightSize.x-heightSize.y, hillPos.y+1+heightSize.x+heightSize.y,
					 	1, tileMap);
		}
	}
	
	//raise the tiles to the given height in the tile map
	private function raiseTiles(startX:int, endX:int, startY:int, endY:int, height:int, tileMap:Array):void
	{
		//exactly bound size
		for (var j:int = startX; j <= endX; j++)
		{
			for (var k:int = startY; k <= endY; k++)
			{
				if (j > tileMap.length)
					break;
				if (! (k > tileMap[0].length))
				{
					//if the original height of the tile is lesser, set to new height
					if (tileMap[j][k].getHeight() < height)
						tileMap[j][k].setHeight(height);
				}
			}
		}
	}
	
//=================== SAND PATCHES =====================
	public function setSandPatch(xPos:int, zPos:int, size:int):void
	{
		_sandPatches.Push(new Vector3(xPos, zPos, size));
	}
	
	private function generateSandPatches(tileMap:Array):void
	{
		for (var i:int = 0; i < _sandPatches.length; i++)
		{
			var pos:Vector3 = _sandPatches[i];
			//+1 to remove the tips
			for (var j:int = pos.x - pos.z + 1; j < pos.x + pos.z; j++)
			{	
				for (var k:int = pos.y - pos.z + 1; k < pos.y + pos.z; k++)
				{
					if (j > tileMap.length)
						break;
					if (!(k > tileMap[0].length))
					{
						//colour the tiles
						if (Mathf.Abs(j-pos.x) + Mathf.Abs(k-pos.y) <= pos.z)
							tileMap[j][k].setTexture(_sandTexture);
					}
				}
			}
			
		}
	}


//================== SET UP ==================	
	
	//get tile, textures and its dimensions
	private function setupTile():void
	{
		//tile
		_tileModel = Resources.Load("Tile");
		_tileLength = _tileModel.renderer.bounds.size.x;
		_tileWidth = _tileModel.renderer.bounds.size.z;
		
		//textures
		_grassTexture = Resources.Load("GrassTexture");
		_sandTexture = Resources.Load("SandTexture");
	}
	
	//Load obstacles and their layout
	private function setupObstacles():void
	{
		_obstacleModels = new Array();
	
		var layout:Array = new Array();
		layout.Push(new Vector2(0,0));
		_obstacleModels.Push(new Obstacle(layout, new Vector2(1,1), Resources.Load("wall1x1")));
		
		layout = new Array();
		layout.push(new Vector2(-1,0));
		layout.push(new Vector2(0,0));
		layout.push(new Vector2(1,0));
		_obstacleModels.Push(new Obstacle(layout, new Vector2(3,1), Resources.Load("logs3x1")));
		
		layout = new Array();
		layout.push(new Vector2(0,-1));
		layout.push(new Vector2(0,0));
		layout.push(new Vector2(0,1));
		_obstacleModels.Push(new Obstacle(layout, new Vector2(1,3), Resources.Load("logs1x3")));
		
		layout = new Array();
		layout.push(new Vector2(0,0));
		layout.push(new Vector2(1,0));
		layout.push(new Vector2(0,1));
		layout.push(new Vector2(1,1));
		_obstacleModels.Push(new Obstacle(layout, new Vector2(2,2), Resources.Load("Crate2x2")));
		
		layout = new Array();
		layout.Push(new Vector2(0,0));
		_obstacleModels.Push(new Obstacle(layout, new Vector2(1,1), Resources.Load("tree1x1")));
		
		layout = new Array();
		for (var i = 0; i < 5; i++)
			for (var j = 0; j < 7; j++)
				layout.Push(new Vector2(i,j));
		_obstacleModels.Push(new Obstacle(layout, new Vector2(5,7), Resources.Load("house5x6")));
	}
}